<?php
/**
 * A php template.
 * To draw template into a string insead of echoing output, simply use this object as string (__toString method is implemented), the template content will be rendered into the string
 * You should not use data property name that begins with an underscore (_), and name '_content' is reserved for child content.
 * <p>Attributes starting with _ (underscore) will not be (html) sanitized.
 *
 * @author Jakub Dykowski
 *        
 */
class Template extends View {
	/**
	 *
	 * @var ArrayObject
	 */
	protected $data;
	
	/**
	 *
	 * @var string
	 */
	protected $path;
	
	/**
	 * Parent view.
	 *
	 * @var Template
	 */
	protected $parent;
	
	/**
	 * Attribute name that is being overriden.
	 *
	 * @var string
	 */
	protected $overridenAttribute = null;
	
	protected $styles;

	/**
	 * The path attribute does not include prefix not suffix(extension).
	 *
	 * @param string $path        	
	 * @throws InvalidArgumentExcepton if $path is not string
	 */
	function __construct($path) {
		if (!is_string($path))
			throw new InvalidArgumentException("path has to be a string");
			// if ($parent !== null && !($parent instanceof self))
			// throw new InvalidArgumentException("parent has to be and app\Template instance");
		$this->path = $path;
		self::loadStyle($path);
		$this->data = new ArrayObject();
	}

	function set($name, $value) {
		$this->data[$name] = $value;
		return $this;
	}

	/**
	 * The path attribute does not include prefix not suffix(extension).
	 *
	 * @param string $name        	
	 * @return ArrayObject
	 */
	function get($name) {
		return $this->data[$name];
	}

	function __set($name, $value) {
		self::set($name, $value);
	}

	function __get($name) {
		return self::get($name);
	}

	/**
	 *
	 * @param string $path
	 *        	path to template
	 */
	function setPath($path) {
		if (!is_string($path))
			throw new InvalidArgumentExcepton("path has to be a string");
		$this->path = strval($path);
	}

	/**
	 *
	 * @return string path to template
	 */
	function getPath() {
		return $this->path;
	}

	/**
	 * Clears this view data, styles, so view can be reused.
	 */
	function clear() {
		$this->data = new ArrayObject();
		unset($this->styles);
	}

	/**
	 * Renders this template into string.
	 */
	function render() {
		ob_start();
		if ($this->parent == null && is_array($this->styles))
			foreach ($this->styles as $style)
				$this->data->_head .= '<link rel="stylesheet" type="text/css" href="' . $style . '">';
		
		foreach ($this->data as $name => $value)
			$$name = \strpos($name, '_') !== 0 ? $this->sanitizeValue($value) : $value;
		require VIEW_TEMPLATE_PATH . $this->path . '.php';
		$rendered = ob_get_clean();
		if ($this->parent !== null) {
			if (isset($this->styles))
				foreach ($this->styles as $style)
					$this->parent->styles[] = $style;
				// $this->data->head = '<link rel="stylesheet" type="text/css" href="'.VIEW_TEMPLATE_PATH.'/' . $style . '">'.$this->data->head;
			foreach ($this->data as $name => $value)
				$this->parent->data->$name = $value;
			$this->parent->data['_content'] = $rendered;
			return $this->parent->render();
		}
		return $rendered;
	}

	/**
	 * Allows to specify default value for attribute which can be overriden in descending templates.
	 * If attribute will be overriden it will be returned otherwise default value will be returned.
	 *
	 * @param string $attribute
	 *        	name
	 * @param mixed $defaultValue
	 *        	to display if not overriden
	 * @return mixed outut value for specified attribute
	 */
	protected function output($attribute, $defaultValue) {
		return isset($this->data->$attribute) ? $this->data->$attribute : $defaultValue;
	}

	function __toString() {
		try {
			return $this->render();
		} catch (Exception $e) {
			// TODO make proper error handling, this one here sucks
			trigger_error($e->getMessage() . ' ' . $e->getTraceAsString(), E_USER_ERROR);
		}
		return ob_get_clean();
	}

	/**
	 * Use inside template body.
	 * Decorates parent template with this template (it will be inserted into parent '_content' data variable).
	 * It HAS to be called ASAP in the template, before enything else is called or rendered.
	 * A template can have only one parent.
	 *
	 * @param string $parent
	 *        	path to parent template
	 */
	protected function extend($path) {
		if (!is_string($path) || !$path)
			throw new Exception("extended template path has to be non empty string");
		$this->parent = new self($path);
	}

	/**
	 * Use inside template body.
	 * Overrides given parent attribute, replacing parent one, or just appends to it.
	 * All template body after this call will be used to override parent attribute. Call finishOverride() to finish overriding.
	 *
	 * @param string $attribute
	 *        	name to override
	 * @param string $value
	 *        	value to replace with or null if start overriding block
	 */
	protected function override($attribute, $value = null) {
		if (is_string(!$attribute))
			throw new \InvalidArgumentException('$attribute has to be non empty string');
		if ($this->parent === null)
			throw new \Exception('to override attribute template need to extend another one');
		if ($value !== null)
			$this->parent->data[$attribute] = $value;
		else {
			$this->overridenAttribute = $attribute;
			ob_start();
		}
	}

	/**
	 *
	 * @param mixed $append
	 *        	whether append(true), prepend otherwise(false), or replace(null)
	 */
	protected function overriden($append = null) {
		if ($this->overridenAttribute == null)
			throw new \Exception('override needs to be called right before overriden()');
		$old = $this->parent->data[$this->overridenAttribute];
		if ($append === null)
			$this->parent->data[$this->overridenAttribute] = ob_get_clean();
		else if ($append)
			$this->parent->data[$this->overridenAttribute] = $old . ob_get_clean();
		else
			$this->parent->data[$this->overridenAttribute] = ob_get_clean() . $old;
		unset($this->overridenAttribute);
	}

	/**
	 * Objects are not sanitized (except \ArrayObject, the rest is htmlspecialchar'ed.
	 *
	 * @param mixed $value        	
	 * @return string string
	 */
	protected static function sanitizeValue($value) {
		if (is_object($value)) {
			return $value;
		} else if (is_array($value) or $value instanceof ArrayObject) {
			return self::htmlspecialcharsOfArray($value);
		} else if ($value != null) {
			return htmlspecialchars($value);
		}
	}

	/**
	 * Helper function, sanitizes values of given array.
	 *
	 * @param unknown $array        	
	 */
	protected static function htmlspecialcharsOfArray($array) {
		foreach ($array as $name => $value) {
			$array[$name] = self::sanitizeValue($value);
		}
	}

	/**
	 * Loads styles for given path.
	 *
	 * @param string $path
	 *        	relative css file path (without extension)
	 */
	protected function loadStyle($path) {
		$pos = 0;
		do {
			$slash = strpos($path, '/', ($pos ? $pos + 1 : 0));
			$cssFile = VIEW_TEMPLATE_PATH . substr($path, 0, ($slash == 0 ? strlen($path) : $slash)) . '.css';
			if (\file_exists($cssFile)) // depending here on globals is bad
				$this->styles[] = $cssFile;
// 				echo "loading file:$path => $cssFile<br>";
			// $this->styles .= '<link rel="stylesheet" type="text/css" href="'.VIEW_TEMPLATE_PATH.'/' . . '">';
			// $GLOBALS['head'][] = '<link rel="stylesheet" type="text/css" href="style/' . $cssFile . '">';
		} while (($pos = $slash) > 0);
	}

	/**
	 * Development version.
	 * Include another (separate) template inside this one.
	 *
	 * @param unknown $path        	
	 * @return string
	 */
	protected function template($path) {
		return VIEW_TEMPLATE_PATH . $path . '.php';
	}
}